Passed
Push — main ( 10d8fd...e2eeed )
by LCS
03:15 queued 01:22
created

Dashboard.tsx ➔ Dashboard   D

Complexity

Conditions 10

Size

Total Lines 169
Code Lines 136

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 136
dl 0
loc 169
rs 4.1999
c 0
b 0
f 0
cc 10

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like Dashboard.tsx ➔ Dashboard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import React, { useEffect, useState } from 'react';
2
import { fetchOwm, fetchAqicn } from '../api';
3
import ChartComp from './ChartComp';
4
import MapComp from './MapComp';
5
import './Dashboard.css';
6
7
type Pollutant = { dt: number; aqi: number };
8
type Coords = { lat: number; lon: number };
9
10
export default function Dashboard() {
11
  // Geolocation & error state
12
  const [coords, setCoords] = useState<Coords | null>(null);
13
  const [error, setError] = useState<string | null>(null);
14
15
  // API data state
16
  const [owmCurr, setOwmCurr] = useState<any>(null);
17
  const [owmF, setOwmF] = useState<Pollutant[]>([]);
18
  const [owmH, setOwmH] = useState<Pollutant[]>([]);
19
  const [aq, setAq] = useState<any>(null);
20
21
  // Ask for GPS once on mount
22
  useEffect(() => {
23
    if (!navigator.geolocation) {
24
      setError('Geolocation is not supported by this browser.');
25
      return;
26
    }
27
    navigator.geolocation.getCurrentPosition(
28
      ({ coords: { latitude, longitude } }) => {
29
        setCoords({ lat: latitude, lon: longitude });
30
      },
31
      (err) => {
32
        let msg: string;
33
        switch (err.code) {
34
          case err.PERMISSION_DENIED:
35
            msg = 'Permission denied. Please allow location access in your browser settings.';
36
            break;
37
          case err.POSITION_UNAVAILABLE:
38
            msg = 'Location information is unavailable.';
39
            break;
40
          case err.TIMEOUT:
41
            msg = 'The request to get your location timed out.';
42
            break;
43
          default:
44
            msg = 'An unknown error occurred.';
45
        }
46
        console.error('Geolocation error', err.code, err.message);
47
        setError(msg);
48
      }
49
    );
50
  }, []);
51
52
  // Fetch AQ data when we have coords
53
  useEffect(() => {
54
    if (!coords) return;
55
    const { lat, lon } = coords;
56
    const now = Math.floor(Date.now() / 1000);
57
58
    fetchOwm('air_pollution', lat, lon).then(setOwmCurr);
59
    fetchOwm('forecast', lat, lon).then(r =>
60
      setOwmF(r.list.map((d: any) => ({ dt: d.dt, aqi: d.main.aqi * 50 })))
61
    );
62
    fetchOwm('history', lat, lon, now - 86400, now).then(r =>
63
      setOwmH(r.list.map((d: any) => ({ dt: d.dt, aqi: d.main.aqi * 50 })))
64
    );
65
    fetchAqicn(lat, lon).then(setAq);
66
  }, [coords]);
67
68
  // Manual‑entry + waiting state
69
  if (!coords) {
70
    return (
71
      <div className="dashboard">
72
        <p>{error || 'Waiting for location…'}</p>
73
        <button
74
          onClick={async () => {
75
            const input = window.prompt(
76
              'Enter location as "lat,lon" or place name (e.g. "Banting, Selangor")'
77
            );
78
            if (!input) return;
79
            const parts = input.split(',').map(s => s.trim());
80
            // If two numbers: coords
81
            if (
82
              parts.length === 2 &&
83
              !isNaN(+parts[0]) &&
84
              !isNaN(+parts[1])
85
            ) {
86
              setCoords({ lat: +parts[0], lon: +parts[1] });
87
              setError(null);
88
              return;
89
            }
90
            // Else try geocoding via OpenStreetMap Nominatim
91
            try {
92
              const resp = await fetch(
93
                `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
94
                  input
95
                )}&format=json&limit=1`
96
              );
97
              const results = await resp.json();
98
              if (results.length === 0) {
99
                setError('Location not found. Try another name.');
100
              } else {
101
                setCoords({
102
                  lat: parseFloat(results[0].lat),
103
                  lon: parseFloat(results[0].lon),
104
                });
105
                setError(null);
106
              }
107
            } catch (e: any) {
108
              console.error('Geocoding error', e);
109
              setError('Geocoding failed. Please try again.');
110
            }
111
          }}
112
        >
113
          Enter location manually
114
        </button>
115
      </div>
116
    );
117
  }
118
119
  // Loading state for API data
120
  if (!owmCurr || !aq) {
121
    return <div className="dashboard">Loading air quality…</div>;
122
  }
123
124
  // Prepare data sources
125
  const sources = [
126
    {
127
      title: 'OpenWeatherMap',
128
      aqi: owmCurr.list[0].main.aqi * 50,
129
      comps: owmCurr.list[0].components,
130
      time: new Date(owmCurr.list[0].dt * 1000).toLocaleString(),
131
    },
132
    {
133
      title: 'AQICN',
134
      aqi: aq.data.aqi,
135
      comps: Object.fromEntries(
136
        Object.entries(aq.data.iaqi).map(([k, v]: any) => [k, v.v])
137
      ),
138
      time: new Date(aq.data.time.iso).toLocaleString(),
139
    },
140
  ];
141
142
  return (
143
    <div className="dashboard">
144
      <h1>AirMerge</h1>
145
146
      <div className="grid">
147
        {sources.map(({ title, aqi, comps, time }) => (
148
          <div
149
            key={title}
150
            className="card"
151
            style={{ borderLeft: `6px solid ${aqColor(aqi)}` }}
152
          >
153
            <h2>{title}</h2>
154
            <p className="aqi">AQI: {aqi}</p>
155
            <p>Time: {time}</p>
156
            <ul className="components">
157
              {Object.entries(comps).map(([pollutant, value]) => (
158
                <li key={pollutant}>
159
                  <span>{pollutant.toUpperCase()}</span>
160
                  <span>{(value as number).toFixed(1)}</span>
161
                </li>
162
              ))}
163
            </ul>
164
          </div>
165
        ))}
166
      </div>
167
168
      <h2>📊 Historical (24h) & Forecast (4d)</h2>
169
      <div className="chart-container">
170
        <ChartComp hist={owmH} fore={owmF} />
171
      </div>
172
173
      <h2>🗺️ Map Overlay</h2>
174
      <div className="map-container">
175
        <MapComp lat={coords.lat} lon={coords.lon} />
176
      </div>
177
    </div>
178
  );
179
}
180
181
function aqColor(aqi: number) {
182
  if (aqi <= 50) return '#009966';
183
  if (aqi <= 100) return '#ffde33';
184
  if (aqi <= 150) return '#ff9933';
185
  if (aqi <= 200) return '#cc0033';
186
  if (aqi <= 300) return '#660099';
187
  return '#7e0023';
188
}
189